using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;

namespace Net.Chirash.Twitter.TwitterDatabase
{
    /// <summary>
    /// Extension methods on ObjectContext.
    /// </summary>
    public static class ObjectContextExtension
    {
        /// <summary>
        /// Attaches an entire objectgraph to the context.
        /// </summary>
        public static T AttachObjectGraph<T>(this ObjectContext context, T entity, params Expression<Func<T, object>>[] paths)
        {
            return AttachObjectGraphs(context, new T[] { entity }, paths)[0];
        }

        /// <summary>
        /// Attaches multiple entire objectgraphs to the context.
        /// </summary>
        public static T[] AttachObjectGraphs<T>(this ObjectContext context, IEnumerable<T> entities, params Expression<Func<T, object>>[] paths)
        {
            T[] unattachedEntities = entities.ToArray();
            T[] attachedEntities = new T[unattachedEntities.Length];
            Type entityType = typeof(T);

            if (unattachedEntities.Length > 0)
            {
                // Workaround to ensure the assembly containing the entity type is loaded:
                // (see: https://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=3405138&SiteID=1)
                try { context.MetadataWorkspace.LoadFromAssembly(entityType.Assembly); }
                catch { }

                #region Automatic preload root entities

                // Create a WHERE clause for preload the root entities:
                StringBuilder where = new StringBuilder("(1=0)");
                List<ObjectParameter> pars = new List<ObjectParameter>();
                int pid = 0;
                foreach (T entity in unattachedEntities)
                {
                    // If the entity has an entitykey:
                    EntityKey entityKey = GetEntityKey(context, entity); // modified by Tsukui_cp
                    if (entityKey != null)
                    {
                        where.Append(" OR ((1=1)");
                        foreach (EntityKeyMember keymember in entityKey.EntityKeyValues)
                        {
                            string pname = String.Format("p{0}", pid++);
                            where.Append(" AND (it.[");
                            where.Append(keymember.Key);
                            where.Append("] = @");
                            where.Append(pname);
                            where.Append(")");
                            pars.Add(new ObjectParameter(pname, keymember.Value));
                        }
                        where.Append(")");
                    }
                }

                // If WHERE clause not empty, construct and execute query:
                if (pars.Count > 0)
                {
                    // Construct query:
                    ObjectQuery<T> query = (ObjectQuery<T>)context.PublicGetProperty(GetEntitySet(context, typeof(T)).Name);
                    foreach (var path in paths)
                        query = query.Include(path);
                    query = query.Where(where.ToString(), pars.ToArray());

                    // Execute query and load entities:
                    //Console.WriteLine(query.ToTraceString());
                    query.Execute(MergeOption.AppendOnly).ToArray();
                }

                #endregion Automatic preload root entities

                // Attach the root entities:
                for (int i = 0; i < unattachedEntities.Length; i++)
                    attachedEntities[i] = (T)context.AddOrAttachInstance(unattachedEntities[i], true);

                // Collect property paths into a tree:
                TreeNode<ExtendedPropertyInfo> root = new TreeNode<ExtendedPropertyInfo>(null);
                foreach (var path in paths)
                {
                    List<ExtendedPropertyInfo> members = new List<ExtendedPropertyInfo>();
                    EntityFrameworkHelper.CollectRelationalMembers(path, members);
                    root.AddPath(members);
                }

                // Navigate over all properties:
                for (int i = 0; i < unattachedEntities.Length; i++)
                    NavigatePropertySet(context, root, unattachedEntities[i], attachedEntities[i]);
            }

            // Return the attached root entities:
            return attachedEntities;
        }

        /// <summary>
        /// Adds or attaches the entity to the context. If the entity has an EntityKey,
        /// the entity is attached, otherwise a clone of it is added.
        /// </summary>
        /// <returns>The attached entity.</returns>
        public static object AddOrAttachInstance(this ObjectContext context, object entity, bool applyPropertyChanges)
        {
            EntityKey entityKey = GetEntityKey(context, entity); // modified by Tsukui_cp
            object attachedEntity = null;
            var found = context.TryGetObjectByKey(entityKey, out attachedEntity);

            if (!found)
            {
                attachedEntity = GetShallowEntityClone(entity);
                context.AddObject(context.GetEntitySet(entity.GetType()).Name, attachedEntity);
                if (entity is IEntityWithKey)
                    ((IEntityWithKey)entity).EntityKey = GetEntityKey(context, attachedEntity); // modified by Tsukui_cp

                return attachedEntity;
            }
            else
            {
                if (applyPropertyChanges)
                {
                    context.ApplyPropertyChanges(entityKey.EntitySetName, entity);
                }
                return attachedEntity;
            }
        }

        /// <summary>
        /// Detaches an objectgraph given it's root object.
        /// </summary>
        /// <returns>The detached root object.</returns>
        public static T DetachObjectGraph<T>(this ObjectContext context, T entity)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                //NetDataContractSerializer serializer = new NetDataContractSerializer();
                //serializer.Serialize(stream, entity);
                //stream.Position = 0;
                //return (T)serializer.Deserialize(stream);
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, entity);
                stream.Position = 0;
                return (T)formatter.Deserialize(stream);
            }
        }

        /// <summary>
        /// Returns the EntitySetName for the given entity type.
        /// Courtesy of SCIP.be.
        /// </summary>
        //public static string GetEntitySetName(this ObjectContext context, Type entityType)
        //{
        //    var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace);
        //    return (from meta in container.BaseEntitySets
        //            where meta.ElementType.FullName == entityType.FullName
        //            select meta.Name).FirstOrDefault();
        //}

        #region Entity path marker methods

        /// <summary>
        /// Marker method to indicate this section of the path expression
        /// should not be loaded but only referenced.
        /// </summary>
        public static object ReferenceOnly(this IEntityWithKey entity)
        {
            throw new InvalidOperationException("The ReferenceOnly() method is a marker method in entity property paths and should not be effectively invoked.");
        }

        /// <summary>
        /// Marker method to indicate the instances the method is called on
        /// within path expressions should not be updated.
        /// </summary>
        public static object WithoutUpdate(this IEntityWithKey entity)
        {
            throw new InvalidOperationException("The WithoutUpdate() method is a marker method in entity property paths and should not be effectively invoked.");
        }

        public static EntitySetBase GetEntitySet(this ObjectContext context, Type entityType)
        {
            if (context == null)
            {
                throw new ArgumentNullException("context");
            }

            if (entityType == null)
            {
                throw new ArgumentNullException("entityType");
            }

            EntityContainer container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace);

            if (container == null)
            {
                return null;
            }

            EntitySetBase entitySet = container.BaseEntitySets.Where(item => item.ElementType.Name.Equals(entityType.Name))
                                                              .FirstOrDefault();

            return entitySet;
        }

        #endregion

        #region Private implementation

        /// <summary>
        /// Navigates a property path on detached instance to translate into attached instance.
        /// </summary>
        private static void NavigatePropertySet(ObjectContext context, TreeNode<ExtendedPropertyInfo> propertynode, object owner, object attachedowner)
        {
            // Try to navigate each of the properties:
            foreach (TreeNode<ExtendedPropertyInfo> childnode in propertynode.Children)
            {
                ExtendedPropertyInfo property = childnode.Item;

                // Retrieve property value:
                object related = property.PropertyInfo.GetValue(owner, null);

                if (related is IEnumerable)
                {
                    // Load current list in context:
                    object attachedlist = property.PropertyInfo.GetValue(attachedowner, null);

                    RelatedEnd relatedEnd = attachedlist as RelatedEnd;
                    if (relatedEnd != null)
                    {
                        EntityState? state = null;
                        if (attachedowner is EntityObject)
                            state = (attachedowner as EntityObject).EntityState;
                        else
                            state = context.ObjectStateManager.GetObjectStateEntry(attachedowner).State;

                        if (state != EntityState.Added && !relatedEnd.IsLoaded)
                            relatedEnd.Load();
                    }

                    // Recursively navigate through new members:
                    List<object> newlist = new List<object>();
                    foreach (var relatedinstance in (IEnumerable)related)
                    {
                        object attachedinstance = relatedinstance;
                        if (context.ObjectStateManager.GetObjectStateEntry(relatedinstance).State != EntityState.Added)
                        {
                            attachedinstance = context.AddOrAttachInstance(relatedinstance, !property.NoUpdate);
                        }
                        newlist.Add(attachedinstance);
                        NavigatePropertySet(context, childnode, relatedinstance, attachedinstance);
                    }

                    // Synchronise lists:
                    List<object> removedItems;
                    SyncList(attachedlist, newlist, out removedItems);

                    // Delete removed items if association is owned:
                    if (AssociationEndBehaviorAttribute.GetAttribute(property.PropertyInfo).Owned)
                    {
                        foreach (var removedItem in removedItems)
                            context.DeleteObject(removedItem);
                    }

                }
                else if (!typeof(IEnumerable).IsAssignableFrom(property.PropertyInfo.PropertyType))
                {
                    // Load reference of currently attached in context:
                    try
                    {
                        RelatedEnd relatedEnd = (RelatedEnd)attachedowner.PublicGetProperty(property.PropertyInfo.Name + "Reference");
                        if (((EntityObject)attachedowner).EntityState != EntityState.Added && !relatedEnd.IsLoaded)
                            relatedEnd.Load();
                    }
                    catch { }

                    // Recursively navigate through new value (unless it's null):
                    object attachedinstance = null;
                    if (related == null)
                        attachedinstance = null;
                    else
                    {
                        if (context.ObjectStateManager.GetObjectStateEntry(related).State != EntityState.Added)
                        {
                            attachedinstance = context.AddOrAttachInstance(related, !property.NoUpdate);
                        }
                        else
                        {
                            attachedinstance = related;
                        }
                        NavigatePropertySet(context, childnode, related, attachedinstance);
                    }

                    // Synchronise value:
                    property.PropertyInfo.SetValue(attachedowner, attachedinstance, null);
                }
            }
        }

        /// <summary>
        /// Returns a shallow clone of only the scalar properties.
        /// </summary>
        private static object GetShallowEntityClone(object entity)
        {
            object clone = Activator.CreateInstance(entity.GetType());
            foreach (PropertyInfo prop in entity.GetType().GetProperties())
            {
                if (typeof(RelatedEnd).IsAssignableFrom(prop.PropertyType)) continue;
                //if (typeof(EntityReference).IsAssignableFrom(prop.PropertyType)) continue;
                if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && (!typeof(String).IsAssignableFrom(prop.PropertyType))) continue;
                if (typeof(IEntityWithKey).IsAssignableFrom(prop.PropertyType)) continue;
                if (typeof(TwitterData).IsAssignableFrom(prop.PropertyType)) continue;
                try
                {
                    prop.SetValue(clone, prop.GetValue(entity, null), null);
                }
                catch
                {
                }
            }
            return clone;
        }

        /// <summary>
        /// Synchronises a targetlist with a sourcelist by adding or removing items from the targetlist.
        /// The targetlist is untyped and controlled through reflection.
        /// </summary>
        private static void SyncList(object targetlist, List<object> sourcelist, out List<object> removedItems)
        {
            List<object> localsourcelist = new List<object>(sourcelist);
            List<object> toremove = new List<object>();

            // Compare both lists:
            foreach (object item in (IEnumerable)targetlist)
            {
                bool found = false;
                for (int i = 0; i < localsourcelist.Count; i++)
                {
                    if (Object.ReferenceEquals(localsourcelist[i], item))
                    {
                        localsourcelist[i] = null;
                        found = true;
                    }
                }
                if (!found)
                    toremove.Add(item);
            }

            // Add members not in targetlist:
            foreach (object item in localsourcelist)
            {
                if (Object.ReferenceEquals(item, null) == false)
                    targetlist.PublicInvokeMethod("Add", item);
            }

            // Remove members not in sourcelist:
            foreach (object item in toremove)
                targetlist.PublicInvokeMethod("Remove", item);

            // Expose removed items:
            removedItems = toremove;
        }

        public static EntityKey GetEntityKey(this ObjectContext context, object obj)
        {
            if (context == null)
                throw new NullReferenceException("ObjectContext is null value");

            if (obj == null)
                return null;

            EntityKey key = obj as EntityKey;
            if (key == null) // detect poco object
            {
                var entitySet = context.GetEntitySet(obj.GetType());
                key = context.CreateEntityKey(entitySet.Name, obj);

                //object c = null;
                //var r = context.TryGetObjectByKey(key, out c);

                //if (!r) // not found 
                //    key = null;
            }
            return key;
        }

        #endregion
    }
}
